从后面的学习中可以看到,useReducer的使用很麻烦,使用起来和redux类似,难度也和redux同一个级别的,而且我感觉useReducer还是会经常用到的,比redux会频繁得多,所以刚开始看不懂不要紧,多看几遍。
当状态更新逻辑较复杂时可以考虑使用 useReducer。useReducer 可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。
什么是reducer呢?


相比于 useState,useReducer 可以更好的描述“如何更新状态”。例如:组件负责发出行为,useReducer 负责更新状态。
好处是:让代码逻辑更清晰,代码行为更易预测。
useReducer 的基础语法如下:
xxxxxxxxxx11const [state, dispatch] = useReducer(reducer, initState, initAction?)其中:

(prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示处理完毕后的新状态。dispatch(action) 方法传入的 action 即可更新 state。定义名为 Father 的父组件如下:
xxxxxxxxxx141import React from 'react'23// 父组件4export const Father: React.FC = () => {5 return (6 <div>7 <button>修改 name 的值</button>8 <div className="father">9 <Son1 />10 <Son2 />11 </div>12 </div>13 )14}定义名为 Son1 和 Son2 的两个子组件如下:
xxxxxxxxxx91// 子组件12const Son1: React.FC = () => {3 return 4}56// 子组件27const Son2: React.FC = () => {8 return 9}在 index.css 中添加对应的样式:
xxxxxxxxxx191.father {2 display: flex;3 justify-content: space-between;4 width: 100vw;5}67.son1 {8 background-color: orange;9 min-height: 300px;10 flex: 1;11 padding: 10px;12}1314.son2 {15 background-color: lightblue;16 min-height: 300px;17 flex: 1;18 padding: 10px;19}按需导入 useReducer 函数:
xxxxxxxxxx11import React, { useReducer } from 'react'定义初始数据:
xxxxxxxxxx11const defaultState = { name: 'liulongbin', age: 16 }定义 reducer 函数,它的作用是:根据旧状态,进行一系列处理,最终返回新状态:
xxxxxxxxxx41const reducer = (prevState) => {2 console.log('触发了 reducer 函数')3 return prevState4}在 Father 组件中,调用 useReducer(reducerFn, 初始状态) 函数,并得到 reducer 返回的状态:
xxxxxxxxxx161// 父组件2export const Father: React.FC = () => {3 // useReducer(fn, 初始数据, 对初始数据进行处理的fn)4 const [state] = useReducer(reducer, defaultState)5 console.log(state)67 return (8 <div>9 <button>修改 name 的值</button>10 <div className="father">11 <Son1 />12 <Son2 />13 </div>14 </div>15 )16}为 reducer 中的 initState 指定数据类型:
xxxxxxxxxx101// 定义状态的数据类型2type UserType = typeof defaultState34const defaultState = { name: 'liulongbin', age: 16 }56// 给 initState 指定类型为 UserType7const reducer = (prevState: UserType) => {8 console.log('触发了 reducer 函数')9 return prevState10}接下来,在 Father 组件中使用 state 时,就可以出现类型的智能提示啦:
xxxxxxxxxx151// 父组件2export const Father: React.FC = () => {3 const [state] = useReducer(reducer, defaultState)4 console.log(state.name, state.age)56 return (7 <div>8 <button>修改 name 的值</button>9 <div className="father">10 <Son1 />11 <Son2 />12 </div>13 </div>14 )15}定义名为 initAction 的处理函数,如果初始数据中的 age 为小数、负数、或 0 时,对 age 进行非法值的处理:
xxxxxxxxxx41const initAction = (initState: UserType) => {2 // 把 return 的对象,作为 useReducer 的初始值3 return { initState, age: Math.round(Math.abs(initState.age)) || 18 }4}在 Father 组件中,使用步骤1声明的 initAction 函数如下:
xxxxxxxxxx71// 父组件2export const Father: React.FC = () => {3 // useReducer(fn, 初始数据, 对初始数据进行处理的fn)4 const [state] = useReducer(reducer, defaultState, initAction)56 // 省略其它代码...7}可以在定义 defaultState 时,为 age 提供非法值,可以看到非法值在 initAction 中被处理掉了。
xxxxxxxxxx241// 父组件2export const Father: React.FC = () => {3 // useReducer(fn, 初始数据, 对初始数据进行处理的fn)4 const [state] = useReducer(reducer, defaultState, initAction)5 console.log(state)67 const onChangeName = () => {8 // 注意:这种用法是错误的,因为不能直接修改 state 的值9 // 因为存储在 useReducer 中的数据都是“不可变”的!10 // 要想修改 useReducer 中的数据,必须触发 reducer 函数的重新计算,11 // 根据 reducer 形参中的旧状态对象(initState),经过一系列处理,返回一个“全新的”状态对象12 state.name = 'escook'13 }1415 return (16 <div>17 <button onClick={onChangeName}>修改 name 的值</button>18 <div className="father">19 <Son1 />20 <Son2 />21 </div>22 </div>23 )24}为了能够触发 reducer 函数的重新执行,我们需要在调用 useReducer() 后接收返回的 dispatch 函数。示例代码如下:
xxxxxxxxxx21// Father 父组件2const [state, dispatch] = useReducer(reducer, defaultState, initAction)在 button 按钮的点击事件处理函数中,调用 dispatch() 函数,从而触发 reducer 函数的重新计算:
xxxxxxxxxx41// Father 父组件2const onChangeName = () => {3 dispatch()4}点击 Father 组件中如下的 button 按钮(如图所示):
修改 name 的值会触发 reducer 函数的重新执行,并打印 reducer 中的 console.log(),代码如下:
xxxxxxxxxx41const reducer = (prevState: UserType) => {2 console.log('触发了 reducer 函数')3 return prevState4}触发了reducer函数,并且组件rerender。

在 Father 父组件按钮的点击事件处理函数 onChangeName 中,调用 dispatch() 函数并把参数传递给 reducer 的第2个形参,代码如下:
xxxxxxxxxx71const onChangeName = () => {2 // 注意:参数的格式为 { type, payload? }3 // 其中:4 // type 的值是一个唯一的标识符,用来指定本次操作的类型,一般为大写的字符串5 // payload 是本次操作需要用到的数据,为可选参数。在这里,payload 指的是把用户名改为字符串 '刘龙彬'6 dispatch({type: 'UPDATE_NAME', payload: '刘龙彬'})7}修改 reducer 函数的形参,添加名为 action 的第2个形参,用来接收 dispatch 传递过来的数据:
xxxxxxxxxx61const reducer = (prevState: UserType, action) => {2 // 打印 action 的值,终端显示的值为:3 // {type: 'UPDATE_NAME', payload: '刘龙彬'}4 console.log('触发了 reducer 函数', action)5 return prevState6}在 reducer 中,根据接收到的 action.type 标识符,决定进行怎样的更新操作,最终 return 一个计算好的新状态。示例代码如下:
xxxxxxxxxx161const reducer = (prevState: UserType, action) => {2 console.log('触发了 reducer 函数', action)3 // return prevState45 switch (action.type) {6 // 如果标识符是字符串 'UPDATE_NAME',则把用户名更新成 action.payload 的值7 // 最后,一定要返回一个新状态,因为 useReducer 中每一次的状态都是“不可变的”8 // 这里使用了对象里面同名属性,后面一个属性会覆盖前面属性的值。ES6语法。9 case 'UPDATE_NAME':10 return { prevState, name: action.payload }11 // 兜底操作:12 // 如果没有匹配到任何操作,则默认返回上一次的旧状态13 default:14 return prevState15 }16}在上述的 switch...case... 代码期间,没有任何 TS 的类型提示,这在大型项目中是致命的。因此,我们需要为 reducer 函数的第2个形参 action 指定操作的类型:
xxxxxxxxxx151// 1. 定义 action 的类型2type ActionType = { type: 'UPDATE_NAME'; payload: string }34// 2. 为 action 指定类型为 ActionType5const reducer = (prevState: UserType, action: ActionType) => {6 console.log('触发了 reducer 函数', action)78 // 3. 删掉之前的代码,再重复编写这段逻辑的时候,会出现 TS 的类型提示,非常 Nice9 switch (action.type) {10 case 'UPDATE_NAME':11 return { prevState, name: action.payload }12 default:13 return prevState14 }15}同时,在 Father 组件的 onChangeName 处理函数内,调用 dispatch() 时也有了类型提示:
xxxxxxxxxx31const onChangeName = () => {2 dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })3}注意:在今后的开发中,正确的顺序是先定义 ActionType 的类型,再修改 reducer 中的 switch…case… 逻辑,最后在组件中调用 dispatch() 函数哦!这样能够充分利用 TS 的类型提示。

在 Father 父组件中,通过展开运算符把 state 数据对象绑定为 Son1 和 Son2 的 props 属性:
xxxxxxxxxx191// 父组件2export const Father: React.FC = () => {3 const [state, dispatch] = useReducer(reducer, defaultState, initAction)45 const onChangeName = () => {6 dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })7 }89 return (10 <div>11 <button onClick={onChangeName}>修改 name 的值</button>12 <div className="father">13 <!-- 通过 props 的数据绑定,把数据传递给子组件 -->14 <Son1 {state} />15 <Son2 {state} />16 </div>17 </div>18 )19}在子组件中,指定 props 的类型为 React.FC<UserType>,并使用 props 接收和渲染数据:
xxxxxxxxxx191// 子组件12const Son1: React.FC<UserType> = (props) => {3 return (4 <div className="son1">5 <p>用户信息:</p>6 <p>{JSON.stringify(props)}</p>7 </div>8 )9}1011// 子组件212const Son2: React.FC<UserType> = (props) => {13 return (14 <div className="son2">15 <p>用户信息:</p>16 <p>{JSON.stringify(props)}</p>17 </div>18 )19}修改完成后,点击父组件中的 button 按钮修改用户名,我们发现两个子组件中的数据同步发生了变化。

扩充 ActionType 的类型如下:
xxxxxxxxxx21// 定义 action 的类型2type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number }在 reducer 中添加 INCREMENT 的 case 匹配:
xxxxxxxxxx131const reducer = (prevState: UserType, action: ActionType) => {2 console.log('触发了 reducer 函数', action)34 switch (action.type) {5 case 'UPDATE_NAME':6 return { prevState, name: action.payload }7 // 添加 INCREMENT 的 case 匹配8 case 'INCREMENT':9 return { prevState, age: prevState.age + action.payload }10 default:11 return prevState12 }13}在子组件 Son1 中添加 +1 的 button 按钮,并绑定点击事件处理函数:
xxxxxxxxxx121// 子组件12const Son1: React.FC<UserType> = (props) => {3 const add = () => {}45 return (6 <div className="son1">7 <p>用户信息:</p>8 <p>{JSON.stringify(props)}</p>9 <button onClick={add}>+1</button>10 </div>11 )12}现在的问题是:子组件 Son1 中无法调用到父组件的 dispatch 函数。为了解决这个问题,我们需要在 Father 父组件中,通过 props 把父组件中的 dispatch 传递给子组件:
xxxxxxxxxx191// 父组件2export const Father: React.FC = () => {3 // useReducer(fn, 初始数据, 对初始数据进行处理的fn)4 const [state, dispatch] = useReducer(reducer, defaultState, initAction)56 const onChangeName = () => {7 dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })8 }910 return (11 <div>12 <button onClick={onChangeName}>修改 name 的值</button>13 <div className="father">14 <Son1 {state} dispatch={dispatch} />15 <Son2 {state} />16 </div>17 </div>18 )19}在 Son1 子组件中,扩充 React.FC<UserType> 的类型,并从 props 中把 dispatch 和用户信息对象分离出来:
xxxxxxxxxx141// 子组件12const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {3 const { dispatch, user } = props45 const add = () => dispatch({ type: 'INCREMENT', payload: 1 })67 return (8 <div className="son1">9 <p>用户信息:</p>10 <p>{JSON.stringify(user)}</p>11 <button onClick={add}>+1</button>12 </div>13 )14}
扩充 ActionType 的类型如下:
xxxxxxxxxx21// 定义 action 的类型2type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number } | { type: 'DECREMENT'; payload: number }在 reducer 中添加 DECREMENT 的 case 匹配:
xxxxxxxxxx151const reducer = (prevState: UserType, action: ActionType) => {2 console.log('触发了 reducer 函数', action)34 switch (action.type) {5 case 'UPDATE_NAME':6 return { prevState, name: action.payload }7 case 'INCREMENT':8 return { prevState, age: prevState.age + action.payload }9 // 添加 DECREMENT 的 case 匹配10 case 'DECREMENT':11 return { prevState, age: prevState.age - action.payload }12 default:13 return prevState14 }15}在子组件 Son2 中添加 -5 的 button 按钮,并绑定点击事件处理函数:
xxxxxxxxxx121// 子组件22const Son2: React.FC<UserType> = (props) => {3 const sub = () => { }45 return (6 <div className="son2">7 <p>用户信息:</p>8 <p>{JSON.stringify(props)}</p>9 <button onClick={sub}>-5</button>10 </div>11 )12}现在的问题是:子组件 Son2 中无法调用到父组件的 dispatch 函数。为了解决这个问题,我们需要在 Father 父组件中,通过 props 把父组件中的 dispatch 传递给子组件:
xxxxxxxxxx191// 父组件2export const Father: React.FC = () => {3 // useReducer(fn, 初始数据, 对初始数据进行处理的fn)4 const [state, dispatch] = useReducer(reducer, defaultState, initAction)56 const onChangeName = () => {7 dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })8 }910 return (11 <div>12 <button onClick={onChangeName}>修改 name 的值</button>13 <div className="father">14 <Son1 {state} dispatch={dispatch} />15 <Son2 {state} dispatch={dispatch} />16 </div>17 </div>18 )19}在 Son2 子组件中,扩充 React.FC<UserType> 的类型,并从 props 中把 dispatch 和用户信息对象分离出来:
xxxxxxxxxx131// 子组件22const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {3 const { dispatch, user } = props4 const sub = () => dispatch({ type: 'DECREMENT', payload: 5 })56 return (7 <div className="son2">8 <p>用户信息:</p>9 <p>{JSON.stringify(user)}</p>10 <button onClick={sub}>-5</button>11 </div>12 )13}
扩充 ActionType 的类型如下:
xxxxxxxxxx21// 定义 action 的类型2type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number } | { type: 'DECREMENT'; payload: number } | { type: 'RESET' }在 reducer 中添加 RESET 的 case 匹配:
xxxxxxxxxx171const reducer = (prevState: UserType, action: ActionType) => {2 console.log('触发了 reducer 函数', action)34 switch (action.type) {5 case 'UPDATE_NAME':6 return { prevState, name: action.payload }7 case 'INCREMENT':8 return { prevState, age: prevState.age + action.payload }9 case 'DECREMENT':10 return { prevState, age: prevState.age - action.payload }11 // 添加 RESET 的 case 匹配12 case 'RESET':13 return defaultState14 default:15 return prevState16 }17}在 GrandSon 组件中,添加重置按钮,并绑定点击事件处理函数:
xxxxxxxxxx101const GrandSon: React.FC<{ dispatch: React.Dispatch<ActionType> }> = (props) => {2 const reset = () => props.dispatch({ type: 'RESET' })34 return (5 <>6 <h3>这是 GrandSon 组件</h3>7 <button onClick={reset}>重置</button>8 </>9 )10}
其实搞清楚了整体构造和流程,使用起来还是蛮简单的,下一步就应该是将代码拆分,构造一个自定义hook,引入useReducer,然后把reducer和action分开到单独的文件中,使用的时候再引入。
说一万遍不如做一遍,下面是上面案例,我的自定义hook做法:
xxxxxxxxxx101// 在hooks文件夹里面创建 types/index.ts 文件,创建并暴露类型、初始值23export const initState = {4 name: "liulongbin",5 age: 22,6};78export type UserType = typeof initState;910export type ActionType = { type: "UPDATE_NAME"; payload: string } | { type: "INCREMENT"; payload: number } | { type: "DECREMENT"; payload: number } | { type: "RESET" };xxxxxxxxxx361// 在hooks文件夹里面新建 useFatherReducer.tsx 文件23import { useReducer } from "react";45import { initState, UserType, ActionType } from "./types/index";67const reducer = (prevState: UserType = initState, action: ActionType) => {8 // 因为这里的type类型是一致的,所以解构赋值出来ts没有报错,但是payload解构赋值出来,会报错,因为payload的类型有多种,如果加类型判断代码的话,会加非常多,所以这里的payload没有使用解构赋值。可以找一下有没有更好的方法。9 const { type } = action;10 switch (type) {11 case "UPDATE_NAME":12 return { prevState, name: action.payload };13 case "INCREMENT":14 return { prevState, age: prevState.age + action.payload };15 case "DECREMENT":16 return { prevState, age: prevState.age - action.payload };17 case "RESET":18 return initState;19 default:20 return prevState;21 }22};2324const initAction = (initState: UserType) => {25 return {26 initState,27 age: Math.round(Math.abs(initState.age)),28 };29};3031export const useFatherReducer = () => {32 const [state, dispatch] = useReducer(reducer, initState, initAction);3334 // 上面从 useReducer 获取到的数据是 [state, dispatch] ,如果我在这里暴露出去是 return [state, dispatch] ,应该也是正常的。但是在组件中使用的时候,会有波浪线,提示 This expression is not callable。具体原因我没有找到,但是我改为 对象的形式暴露出去之后,就不报错了。说到底还是typescript没有玩好啊。35 return { state, dispatch };36};记录一种报错:
xxxxxxxxxx731// 在组件中使用23import React from "react";4import { useFatherReducer } from "@/hooks/useFatherReducer";5import type { UserType, ActionType } from "./types/index";67const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {8 const { dispatch, user } = props;910 const add = () => {11 dispatch({ type: "INCREMENT", payload: 1 });12 };1314 return (15 <div className="son1">16 <h3>Son1 组件</h3>17 <p>{JSON.stringify(user)}</p>18 <button onClick={add}>点我年龄+1</button>19 <hr />20 <GrandSon dispatch={dispatch} />21 </div>22 );23};2425const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {26 const { dispatch, user } = props;2728 const decrement = () => {29 dispatch({ type: "DECREMENT", payload: 1 });30 };3132 return (33 <div className="son2">34 <h3>Son2 组件</h3>35 <p>{JSON.stringify(user)}</p>36 <button onClick={decrement}>点我年龄-1</button>37 </div>38 );39};4041const GrandSon: React.FC<{ dispatch: React.Dispatch<ActionType> }> = (props) => {42 const { dispatch } = props;4344 const reset = () => {45 dispatch({ type: "RESET" });46 };4748 return (49 <>50 <h3>这是 GrandSon 组件</h3>51 <button onClick={reset}>重置</button>52 </>53 );54};5556export const Father: React.FC = () => {57 const { state, dispatch } = useFatherReducer();5859 const onChangeName = () => {60 dispatch({ type: "UPDATE_NAME", payload: "刘龙彬" });61 };6263 return (64 <>65 <h2>Father组件</h2>66 <button onClick={onChangeName}>更改姓名</button>67 <div className="father">68 <Son1 {state} dispatch={dispatch} />69 <Son2 {state} dispatch={dispatch} />70 </div>71 </>72 );73};

这样做的好处就是:关注点分离。逻辑写好之后,只需要调用就行了,否则UI和逻辑混在一起,非常难理解。
安装 immer 相关的依赖包:
xxxxxxxxxx11npm install immer use-immer -S从 use-immer 中导入 useImmerReducer 函数,并替换掉 React 官方的 useReducer 函数的调用:
xxxxxxxxxx81// 1. 导入 useImmerReducer2import { useImmerReducer } from 'use-immer'34// 父组件5export const Father: React.FC = () => {6 // 2. 把 useReducer() 的调用替换成 useImmerReducer()7 const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)8}修改 reducer 函数中的业务逻辑,case 代码块中不再需要 return 不可变的新对象了,只需要在 prevState 上进行修改即可。Immer 内部会复制并返回新对象,因此降低了用户(react的用户就是程序员)的心智负担。改造后的 reducer 代码如下:
xxxxxxxxxx221const reducer = (prevState: UserType, action: ActionType) => {2 console.log('触发了 reducer 函数', action)34 switch (action.type) {5 case 'UPDATE_NAME':6 // return { ...prevState, name: action.payload }7 prevState.name = action.payload8 break9 case 'INCREMENT':10 // return { ...prevState, age: prevState.age + action.payload }11 prevState.age += action.payload12 break13 case 'DECREMENT':14 // return { ...prevState, age: prevState.age - action.payload }15 prevState.age -= action.payload16 break17 case 'RESET':18 return defaultState19 default:20 return prevState21 }22}
在 react 函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给最深层的子组件时,传统的办法是使用 props,一层一层把数据向下传递。
使用 props 层层传递数据的维护性太差了,我们可以使用 React.createContext() + useContext() 轻松实现多层组件的数据传递。

主要的使用步骤如下:
XxxxContext的形式,因为这个变量是要用作标签的,所以首字母要大写。初始数据一般定义为空对象。<XxxxContext.Provider value={}>{/**...各种子组件*/}</XxxxContext.Provider> 提供数据useContext(XxxxContext) 接收数据。xxxxxxxxxx211import React, { useContext } from 'react'23// 全局。初始数据一般定义为 {},真正传递的值在标签的value属性中传递。4const MyContext = React.createContext(初始数据)56// 父组件7const Father = () => {8 return <MyContext.Provider value={{name: 'escook', age: 22}}>9 {/** 省略其它代码 */}10 <Son />11 </MyContext.Provider>12}1314// 子组件15const Son = () => {16 const myCtx = useContext(MyContext)17 return <div>18 <p>姓名:{myCtx.name}</p>19 <p>年龄:{myCtx.age}</p>20 </div>21}执行效果:

定义 LevelA,LevelB,LevelC 的组件结构如下:
xxxxxxxxxx331import React, { useState } from 'react'23export const LevelA: React.FC = () => {4 // 定义状态5 const [count, setCount] = useState(0)67 return (8 <div style={{ padding: 30, backgroundColor: 'lightblue', width: '50vw' }}>9 <p>count值是:{count}</p>10 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>11 {/* 使用子组件 */}12 <LevelB />13 </div>14 )15}1617export const LevelB: React.FC = () => {18 return (19 <div style={{ padding: 30, backgroundColor: 'lightgreen' }}>20 {/* 使用子组件 */}21 <LevelC />22 </div>23 )24}2526export const LevelC: React.FC = () => {27 return (28 <div style={{ padding: 30, backgroundColor: 'lightsalmon' }}>29 <button>+1</button>30 <button>重置</button>31 </div>32 )33}在父组件中,调用 React.createContext 向下共享数据;在子组件中调用 useContext() 获取数据。示例代码如下:
xxxxxxxxxx441import React, { useState, useContext } from 'react'23// 声明 TS 类型4type ContextType = { count: number; setCount: React.Dispatch<React.SetStateAction<number>> }56// 1. 创建 Context 对象7const AppContext = React.createContext<ContextType>({} as ContextType)89export const LevelA: React.FC = () => {10 const [count, setCount] = useState(0)1112 return (13 <div style={{ padding: 30, backgroundColor: 'lightblue', width: '50vw' }}>14 <p>count值是:{count}</p>15 <button onClick={() => setCount((prev) => prev + 1)}>+1</button>16 {/* 2. 使用 Context.Provider 向下传递数据 */}17 <AppContext.Provider value={{ count, setCount }}>18 <LevelB />19 </AppContext.Provider>20 </div>21 )22}2324export const LevelB: React.FC = () => {25 return (26 <div style={{ padding: 30, backgroundColor: 'lightgreen' }}>27 <LevelC />28 </div>29 )30}3132export const LevelC: React.FC = () => {33 // 3. 使用 useContext 接收数据34 const ctx = useContext(AppContext)3536 return (37 <div style={{ padding: 30, backgroundColor: 'lightsalmon' }}>38 {/* 4. 使用 ctx 中的数据和方法 */}39 <p>count值是:{ctx.count}</p>40 <button onClick={() => ctx.setCount((prev) => prev + 1)}>+1</button>41 <button onClick={() => ctx.setCount(0)}>重置</button>42 </div>43 )44}
重要:
如果不知道react中某些变量的类型,特别是以前从来没有见过、用过的API,该怎么办?
1、首先是查文档,看文档里面是怎么定义的。
2、如果什么文档都查不到,那么就看vscode里面的提示,这个提示还是蛮有用的,先按照提示写上去,等有问题再来改。
如果要我查找文档的话,我还真不知道该怎么查。这时候可以将鼠标放在setCount上面,会有类型的提示:
不敢保证vscode的提示100%正确,但是先把提示的类型写上去,有问题再改,这也是个好方法。
3、react的API定义的变量类型,要区分是返回值类型还是参数类型,确实很复杂,但再复杂也要搞清楚。
比如说:
xxxxxxxxxx131const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {2const { dispatch, user } = props;3const add = () => {4dispatch({ type: "INCREMENT", payload: 1 });5};6return (7<div className="son1">8<p>用户信息:</p>9<p>{JSON.stringify(user)}</p>10<button onClick={add}>点我年龄+1</button>11</div>12);13};这里的React.FC是返回值类型,
<>里面的是参数类型,这应该是typescript的基础知识,只不过我一直没有使用,所以很不熟悉。
-----2024.02.22
还是要会看官方typescript定义,鼠标选中某个react的方法,右键选择“Go to Type Definition”,就可以跳转到官方定义的文件里面去:
会发现有两个同名的类型定义,这是为什么呢?
上面一个是右键点击“Go to Definition”会高亮的地方,下面一个是右键点击“Go to Type Definition”会高亮的地方,这个应该是函数重载,真正的定义应该在“Go to source definition”跳转到的位置,但是我看了一下,跳转到的位置的函数定义也是很简短,不知道经过什么方式处理了。
在刚才的案例中,我们发现父组件 LevelA 为了向下传递共享的数据,在代码中侵入了 <AppContext.Provider> 这样的代码结构。
为了保证父组件中代码的单一性,也为了提高 Provider 的通用性,我们可以考虑把 Context.Provider 封装到独立的 Wrapper 函数式组件中,将Wrapper组件做成一个单独的传递值的组件,所有子组件都要用到的值,只定义在Wrapper组件中,这样就可以做到业务与数据分离(当然实际情况也许没有这么完美,子组件中完全可以自定义值、获取数据来使用),例如:
xxxxxxxxxx161// wrapper.tsx文件23// 声明 TS 类型4type ContextType = { count: number; setCount: React.Dispatch<React.SetStateAction<number>> }5// 创建 Context 对象6export const AppContext = React.createContext<ContextType>({} as ContextType)78// 定义独立的 Wrapper 组件,被 Wrapper 嵌套的子组件会被 Provider 注入数据9export const AppContextWrapper: React.FC<React.PropsWithChildren> = (props) => {10 // 1. 定义要共享的数据11 const [count, setCount] = useState(0)12 // 2. 使用 AppContext.Provider 向下共享数据。这里的 {props.children} 是必须的,因为这里表示的是AppContextWrapper组件里面是有子组件的,相当于是插槽。13 return <AppContext.Provider value={{ count, setCount }}>14 {props.children}15 </AppContext.Provider>16}这里的AppContextWrapper参数类型也不好定义,怎么办?无论鼠标指向props,还是props.children,都没有很好的提示:
------2024.02.23
原因是什么呢?原因是我没有在AppContextWrapper组件的FC类型里面指定泛型定义,写成这样就会有提示了:
xxxxxxxxxx161import React, { useState } from "react"23type AppContextType = {4count:number;5setCount: React.Dispatch<React.SetStateAction<number>>6}78const AppContext = React.createContext<AppContextType>({} as AppContextType)910export const AppContextWrapper: React.FC<React.PropsWithChildren> = (props) => {11const [count, setCount] = useState(0)1213return <AppContext.Provider value={{count, setCount}}>14{props.children}15</AppContext.Provider>16}
定义好 Wrapper 组件后,我们可以在 App.tsx 中导入并使用 Wrapper 和 LevelA 组件,代码如下:
xxxxxxxxxx141import React from 'react'2import { AppContextWrapper, LevelA } from '@/components/use_context/01.base.tsx'34const App: React.FC = () => {5 return (6 <AppContextWrapper>7 <!-- AppContextWrapper 中嵌套使用了 LevelA 组件,形成了父子关系 -->8 <!-- LevelA 组件会被当做 children 渲染到 Wrapper 预留的插槽中 -->9 <LevelA />10 </AppContextWrapper>11 )12}1314export default App这样,组件树的嵌套关系为:App => Wrapper => LevelA => LevelB => LevelC。因此在 LevelA、LevelB 和 LevelC 组件中,都可以使用 context 中的数据。例如,LevelA 组件中的代码如下:
xxxxxxxxxx151import { AppContext } from "./wrapper"23export const LevelA: React.FC = () => {4 // 使用 useContext 接收数据。注意:这里使用的是 React.createContext 定义的变量,而不是创建的 wrapper 组件。这一点一定要理解。5 const ctx = useContext(AppContext)67 return (8 <div style={{ padding: 30, backgroundColor: 'lightblue', width: '50vw' }}>9 {/* 使用 ctx 中的数据和方法 */}10 <p>count值是:{ctx.count}</p>11 <button onClick={() => ctx.setCount((prev) => prev + 1)}>+1</button>12 <LevelB />13 </div>14 )15}LevelC 组件中的代码如下:
xxxxxxxxxx151import { AppContext } from "./wrapper"23export const LevelC: React.FC = () => {4 // 使用 useContext 接收数据5 const ctx = useContext(AppContext)67 return (8 <div style={{ padding: 30, backgroundColor: 'lightsalmon' }}>9 {/* 使用 ctx 中的数据和方法 */}10 <p>count值是:{ctx.count}</p>11 <button onClick={() => ctx.setCount((prev) => prev + 1)}>+1</button>12 <button onClick={() => ctx.setCount(0)}>重置</button>13 </div>14 )15}核心思路:每个 Context 都创建一个对应的 Wrapper 组件,在 Wrapper 组件中使用 Provider 向 children 注入数据。
定义 Context 要向下共享的数据的 TS 类型,代码如下:
xxxxxxxxxx31// 1. 定义 Context 的 TS 类型2// 在这一步,我们必须先明确要向子组件注入的数据都有哪些3type UserInfoContextType = { user: UserType; dispatch: React.Dispatch }使用 React.createContext 创建 Context 对象:
xxxxxxxxxx21// 2. 创建 Context 对象2const UserInfoContext = React.createContext<UserInfoContextType>({} as UserInfoContextType)创建 ContextWrapper 组件如下,把 Father 组件中的 useImmerReducer 调用过程,抽离到 ContextWrapper 中:
xxxxxxxxxx51// 3. 创建 ContextWrapper 组件2export const UserInfoContextWrapper: React.FC = ({ children }) => {3 const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)4 return {children}5}改造 Father 组件,调用 useContext 获取并使用 Context 中的数据。同时,Father 组件也不必再使用 props 把 state 和 dispatch 函数传递给 Son 子组件:
xxxxxxxxxx201export const Father: React.FC = () => {2 // 4. 调用 useContext 导入需要的数据3 const { user: state, dispatch } = useContext(UserInfoContext)45 const changeUserName = () => dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })67 return (8 <div>9 <button onClick={changeUserName}>修改用户名</button>10 <p>{JSON.stringify(state)}</p>11 <div className="father">12 {/* 5. 这里没有必要再往子组件传递 props 了 */}13 {/* <Son1 {...state} dispatch={dispatch} />14 <Son2 {...state} dispatch={dispatch} /> */}15 <Son1 />16 <Son2 />17 </div>18 </div>19 )20}改造 App 根组件,分别导入 UserInfoContextWrapper 和 Father 组件,并形成父子关系的嵌套,这样 Father 组件及其子组件才可以访问到 Context 中的数据:
xxxxxxxxxx121import React from 'react'2import { UserInfoContextWrapper, Father } from '@/components/use_reducer/01.base.tsx'34const App: React.FC = () => {5 return (6 <UserInfoContextWrapper>7 <Father />8 </UserInfoContextWrapper>9 )10}1112export default App最后,改造 Son1,Son2 和 GrandSon 组件,删除 props 及其类型定义,改用 useContext() 来获取 UserInfoContextWrapper 向下注入的数据。示例代码如下:
xxxxxxxxxx421const Son1: React.FC = () => {2 // 6. 把 props 替换为 useContext() 的调用3 const { dispatch, user } = useContext(UserInfoContext)45 const add = () => dispatch({ type: 'INCREMENT', payload: 1 })67 return (8 <div className="son1">9 <p>{JSON.stringify(user)}</p>10 <button onClick={add}>年龄+1</button>11 </div>12 )13}1415const Son2: React.FC = () => {16 // 7. 把 props 替换为 useContext() 的调用17 const { dispatch, user } = useContext(UserInfoContext)1819 const sub = () => dispatch({ type: 'DECREMENT', payload: 5 })2021 return (22 <div className="son2">23 <p>{JSON.stringify(user)}</p>24 <button onClick={sub}>年龄-5</button>25 <hr />26 <GrandSon />27 </div>28 )29}3031const GrandSon: React.FC = () => {32 // 8. 把 props 替换为 useContext() 的调用33 const { dispatch } = useContext(UserInfoContext)34 const reset = () => dispatch({ type: 'RESET' })3536 return (37 <>38 <h3>这是 GrandSon 组件</h3>39 <button onClick={reset}>重置</button>40 </>41 )42}完整版代码如下:
xxxxxxxxxx561// UserWrapper.tsx23import React from "react";45// 引入useImmerReducer代替useReducer6import { useImmerReducer } from "use-immer";78// 定义action的类型9type ActionType = { type: "UPDATE_NAME"; payload: string } | { type: "INCREMENT"; payload: number } | { type: "DECREMENT"; payload: number } | { type: "RESET" };1011// 定义state的类型12type UserType = typeof defaultState;1314// 定义context返回值的类型15type UserInfoContextType = { state: UserType; dispatch: React.Dispatch<ActionType> };1617// 初始值18const defaultState = {19 name: "tom",20 age: 18,21};2223// 定义并暴露Context24export const UserContext = React.createContext<UserInfoContextType>({} as UserInfoContextType);2526// 定义初始值的处理函数27const initAction = (defaultState: UserType) => {28 return { defaultState, age: Math.round(Math.abs(defaultState.age)) || 18 };29};3031// 定义reducer32const reducer = (prevState: UserType, action: ActionType) => {33 switch (action.type) {34 case "UPDATE_NAME":35 prevState.name = action.payload;36 break;37 case "INCREMENT":38 prevState.age += action.payload;39 break;40 case "DECREMENT":41 prevState.age -= action.payload;42 break;43 case "RESET":44 return initAction(defaultState);45 default:46 return prevState;47 }48};4950export const UserContextWrapper: React.FC<{51 children?: React.ReactNode;52}> = ({ children }) => {53 const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction);5455 return <UserContext.Provider value={{ state, dispatch }}>{children}</UserContext.Provider>;56};xxxxxxxxxx781// father.tsx23import React, { useContext } from "react";4import { UserContext } from "./userWrapper";56// 定义父组件7export const Father: React.FC = () => {8 // 从context中获取状态和dispatch函数,这里使用了对象的解构赋值和取别名9 const { state: user, dispatch } = useContext(UserContext);1011 const changeUserName = () => {12 dispatch({13 type: "UPDATE_NAME",14 payload: "刘德华",15 });16 };17 18 return (19 <div>20 <button onClick={changeUserName}>修改用户名</button>21 <p>{JSON.stringify(user)}</p>22 <div className="father">23 <Son1 />24 <Son2 />25 </div>26 </div>27 );28};2930const Son1: React.FC = () => {31 const { state: user, dispatch } = useContext(UserContext);3233 const add = () => {34 dispatch({35 type: "INCREMENT",36 payload: 1,37 });38 };3940 return (41 <div className="son1">42 <p>{JSON.stringify(user)}</p>43 <button onClick={add}>年龄+1</button>44 </div>45 );46};4748const Son2: React.FC = () => {49 const { state: user, dispatch } = useContext(UserContext);5051 const sub = () => {52 dispatch({53 type: "DECREMENT",54 payload: 1,55 });56 };5758 return (59 <div className="son2">60 <p>{JSON.stringify(user)}</p>61 <button onClick={sub}>年龄-1</button>62 <hr />63 <GrandSon />64 </div>65 );66};6768const GrandSon: React.FC = () => {69 const { dispatch } = useContext(UserContext);7071 return (72 <>73 <h3>这是 GrandSon 组件</h3>74 <button onClick={() => dispatch({ type: "RESET" })}>重置</button>75 </>76 );77};78xxxxxxxxxx141// App.tsx23import { Father } from '@/components/05_use_context/father'4import { UserContextWrapper } from '@/components/05_use_context/userWrapper'56function App() {7 return (8 <UserContextWrapper>9 <Father />10 </UserContextWrapper>11 );12}1314export default App;执行效果:
